package org.box.metadata.cli;

import java.lang.reflect.Field;

import org.box.metadata.cli.annotation.Argument;
import org.box.metadata.cli.annotation.Arguments;
import org.box.metadata.cli.annotation.CLIConfiguration;
import org.box.metadata.cli.annotation.Command;
import org.box.metadata.cli.annotation.Option;
import org.box.metadata.cli.exception.InvalidConfigurationException;
import org.box.metadata.cli.exception.ParseException;
import org.box.metadata.cli.impl.CommandCompatible;
import org.box.metadata.cli.impl.GnuCommandsSupportParser;
import org.box.metadata.cli.util.Util;

/**
 * 
 * Annotation's based {@link CommandLineParser} implementation for Application
 * Command Line Interface. MetaCommandLineParser uses meta information
 * represented via java annotations in an application's configuration class.
 * 
 * Simple application's configuration class can be demonstrated by the code
 * snippet below:
 * 
 * <pre>
 * &#064;CLIConfiguration()
 * public class AppConfiguration {
 * 
 * 	&#064;Option(description = &quot;Simple private yes/no option without agruments&quot;, fullName = &quot;key1&quot;, shortName = &quot;k1&quot;)
 * 	private boolean key1;
 * 
 * 	&#064;Option(description = &quot;Simple key/value option with agruments&quot;, fullName = &quot;keyValue1&quot;, shortName = &quot;kv1&quot;, hasArguments = true)
 * 	public String keyValue1;
 * 
 * 	&#064;Argument(index = 1, name = &quot;infile&quot;, required = true)
 * 	public String infile;
 * 
 * }
 * </pre>
 * 
 * Parser analyzes User's input and creates a new AppConfiguration instance with
 * injected values into corresponding fields.
 * 
 * <pre>
 * CommandLineParser&lt;AppConfiguration&gt; cliParser = new MetaCommandLineParser&lt;AppConfiguration&gt;(
 * 		AppConfiguration.class);
 * 
 * AppConfiguration cfg = cliParser.parse(args);
 * </pre>
 * 
 * or you can simply call:
 * 
 * <pre>
 * DummyConfiguration cfg = MetaCli.parse(args, DummyConfiguration.class);
 * </pre>
 * 
 * @author <a href="mailto:alexander.box@gmail.com">Alexander Korobka</a>
 * 
 */
public class MetaCommandLineParser<T> implements CommandLineParser<T> {

	/**
	 * Enum of all out of box parser supported in the Apache's commons-cli
	 * library.
	 * 
	 */
	public enum ParserType {
		BASIC_PARSER(new org.apache.commons.cli.BasicParser()), //
		GNU_PARSER(new org.apache.commons.cli.GnuParser()), //
		GNU_COMMAND_SUPPORT_PARSER(new GnuCommandsSupportParser()), //
		POSIX_PARSER(new org.apache.commons.cli.PosixParser());//

		final org.apache.commons.cli.Parser parser;

		private ParserType(org.apache.commons.cli.Parser parser) {
			this.parser = parser;
		}
	}
	
	private final Class<T> cfgClass;

	private final org.apache.commons.cli.CommandLineParser delegate;

	private boolean incCommonOptions = true;

	/**
	 * Constructors a new instance of the GnuParser.
	 * 
	 * @param cfgClass
	 *            - a class of the Application's configuration
	 */
	public MetaCommandLineParser(Class<T> cfgClass) {
		this(cfgClass, ParserType.GNU_PARSER);
	}

	/**
	 * Constructors a new instance of the parser with the given type.
	 * 
	 * @param cfgClass
	 *            - a class of the Application's configuration
	 * @param parserType
	 *            - one of the following {@link ParserType}
	 */
	public MetaCommandLineParser(Class<T> cfgClass, ParserType parserType) {
		this.cfgClass = cfgClass;
		delegate = parserType.parser;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.box.metadate.cli.CommandLineParser#parse(java.lang.String[])
	 */
	public T parse(String[] args) throws ParseException {

		validateClass();
		Class<?> clazz = getCfgClass();
		org.apache.commons.cli.CommandLine cl = doParse(clazz, args);

		Field cField = null;
		if (delegate instanceof CommandCompatible) {
			cField = getCommandField(
					getCfgClass(), 
					((CommandCompatible) delegate).findCommand(cfgClass, args));

			if (cField != null)
				clazz = cField.getClass();
		}

		validateArguments(cl, clazz);
		return doCreateConfigurationObject(cField, cl);
	}

	/**
	 * Validates configuration's class
	 * 
	 */
	protected void validateClass() {
		if (!getCfgClass().isAnnotationPresent(CLIConfiguration.class)) {
			throw new InvalidConfigurationException(
					"Invalid configuration class. Configuration class must heve 'CLIConfiguration' annotation");
		}
	}

	/**
	 * Do parsing on the delegate's site and return Apache's CommandLine object
	 * for the following decompiling.
	 * 
	 * @param clazz
	 * @param args
	 * 
	 * @return
	 * @throws ParseException
	 */
	protected org.apache.commons.cli.CommandLine doParse(Class<?> clazz, String[] args) throws ParseException {
		try {
			if (delegate instanceof CommandCompatible) {
				return ((CommandCompatible) delegate).parse(isIncCommonOptions(), clazz, args);
			} else {
				return delegate.parse(createCLIOptions(getCfgClass()), args);
			}
		} catch (org.apache.commons.cli.ParseException e) {
			throw new ParseException(e.getMessage());
		}
	}

	protected T doCreateConfigurationObject(Field cField,
			org.apache.commons.cli.CommandLine cl) {

		try {

			T cfg = getCfgClass().newInstance();
			if (cField != null) {
				Object command = cField.getType().newInstance();
				cField.setAccessible(true);
				cField.set(cfg, command);

				injectValues(cfg, cl, false);
				injectValues(command, cl, true);
			} else {
				injectValues(cfg, cl, true);
			}

			return cfg;
		} catch (InstantiationException e) {
			throw new InvalidConfigurationException(e.getMessage());
		} catch (IllegalAccessException e) {
			throw new InvalidConfigurationException(e.getMessage());
		}
	}

	/**
	 * Injects Command Line options and arguments into the Application's
	 * configuration object
	 * 
	 * @param cfg
	 *            - the instance of Application's configuration or a
	 *            configuration's command
	 * @param cmd
	 *            - CommandLine from delegated parser
	 * @param populateArgs
	 *            - populate arguments fields too, or just options
	 * 
	 * 
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	protected void injectValues(Object cfg,
			org.apache.commons.cli.CommandLine cmd, boolean populateArgs) {

		try {
			for (Field f : cfg.getClass().getDeclaredFields()) {
				// populate options
				if (f.isAnnotationPresent(Option.class)) {
					Option o = f.getAnnotation(Option.class);
					if (cmd.hasOption(o.shortName())) {
						f.setAccessible(true);
						if (o.hasArguments()) {
							f.set(cfg, cmd.getOptionValue(o.shortName()));
						} else {
							f.setBoolean(cfg, true);
						}
					}
				}

				if (populateArgs) {
					// populate enumerated arguments
					String[] args = cmd.getArgs();
					if (f.isAnnotationPresent(Argument.class)) {
						Argument a = f.getAnnotation(Argument.class);
						if (a.index() <= args.length) {
							f.setAccessible(true);
							f.set(cfg, args[a.index() - 1]);
						}
					}

					// non-enumerated args
					if (f.isAnnotationPresent(Arguments.class)) {
						f.setAccessible(true);
						f.set(cfg, args);
					}
				}
			}
		} catch (IllegalArgumentException e) {
			throw new InvalidConfigurationException(e.getMessage());
		} catch (IllegalAccessException e) {
			throw new InvalidConfigurationException(e.getMessage());
		}

	}

	/**
	 * This is bran new (not supported in commons-cli) validation of CLI
	 * arguments annotated via {@link Argument} and {@link Arguments}
	 * annotations.
	 * 
	 * @param cmd
	 *            - a result of delegated parser processing
	 * @param clazz
	 *            - a class that contains arguments' declarations. That could be
	 *            a class of the underlined configuration or a one of
	 *            configurations' commands
	 * 
	 * @throws ParseException
	 */
	protected void validateArguments(org.apache.commons.cli.CommandLine cmd,
			Class<?> clazz) throws ParseException {

		String[] args = cmd.getArgs();
		for (Field f : clazz.getDeclaredFields()) {
			// check enumerated arguments
			if (f.isAnnotationPresent(Argument.class)) {
				Argument arg = f.getAnnotation(Argument.class);
				if (arg.required() && arg.index() > args.length) {
					throw new ParseException(String.format(
							"Argument '%s' is not specified", arg.name()));
				}
			}
		}
	}

	/**
	 * Creates a Options list for the delegated parser
	 * 
	 * @param clazz
	 *            - a class that contains options' declarations. That could be a
	 *            class of the underlined configuration or a one of
	 *            configurations' commands
	 * 
	 * @return a list of options from the given class
	 * 
	 * @throws ParseException
	 * @throws InvalidConfigurationException
	 *             if the given class is not compatible or valid as
	 *             Application's configuration
	 */
	protected org.apache.commons.cli.Options createCLIOptions(Class<?> clazz)
			throws ParseException, InvalidConfigurationException {
		return Util.createCLIOptions(clazz);
	}

	protected Field getCommandField(Class<?> cfgClazz, Command command) {
		return Util.getCommandField(command, cfgClazz);
	}

	/**
	 * A getter for <code>cfgClass</code> property
	 * 
	 * @return a class of the underlined configuration
	 */
	public Class<T> getCfgClass() {
		return cfgClass;
	}

	public Command findCommandIn(Class<?> cfgClass, String[] args) throws ParseException {
		if (!(delegate instanceof CommandCompatible))
			throw new ParseException("Commands are not supported for the given parser type.");
		
		return ((CommandCompatible)delegate).findCommand(cfgClass, args);
	}

	public boolean isIncCommonOptions() {
		return incCommonOptions;
	}

	public void setIncCommonOptions(boolean incCommonOptions) {
		this.incCommonOptions = incCommonOptions;
	}

}
